#include <glad/glad.h>
#include <GLFW/glfw3.h>

#include <iostream>

/**
 * Q: 1. 顶点着色器和片段着色器是什么？
 * D: 顶点着色器和片段着色器中间用 链接器 连接，链接器将顶点着色器的输出和片段着色器的输入连接在一起。
 *    着色器代码编写语言是GLSL，它是一种类似C语言的语言，专门用于编写着色器。
 * 
 * Q：2. VAO和VBO的概念？
 * D: VAO看成是一个容器，里面存放了多个VBO，每个VBO存放了一个顶点属性的数据。
 *    顶点属性可以是位置、颜色、法线、纹理坐标等。
 */

void processInput(GLFWwindow *window)
{
    if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)
        glfwSetWindowShouldClose(window, true);
}

void framebuffer_size_callback(GLFWwindow* window, int width, int height)
{
    std::cout << "framebuffer_size_callback" << std::endl;
    glViewport(0, 0, width, height);
}

// settings
const unsigned int SCR_WIDTH = 800;
const unsigned int SCR_HEIGHT = 600;

// 顶点着色器源码
const char *vertexShaderSource = "#version 330 core\n"
    "layout (location = 0) in vec3 aPos;\n"
    "void main()\n"
    "{\n"
    "   gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);\n"
    "}\0";

// 片段着色器源码
const char *fragmentShaderSource = "#version 330 core\n"
    "out vec4 FragColor;\n"
    "void main()\n"
    "{\n"
    "   FragColor = vec4(1.0f, 0.5f, 0.2f, 1.0f);\n"
    "}\n\0";

int main()
{
    glfwInit(); // 初始化GLFW
    glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3); // 设置主版本号
    glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3); // 设置次版本号
    glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); // 设置OpenGL为核心模式

#ifdef __APPLE__
    glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE); 
#endif

    // 创建一个800x600像素的窗口，窗口标题为 "LearnOpenGL"
    GLFWwindow* window = glfwCreateWindow(SCR_WIDTH, SCR_HEIGHT, "LearnOpenGL", NULL, NULL);
    if (window == NULL)
    {
        std::cout << "Failed to create GLFW window" << std::endl;
        glfwTerminate(); // 清理所有由GLFW分配的资源
        return -1;
    }
    // 设置之前创建的 window 为当前线程的主上下文。这意味着之后所有的OpenGL调用都会作用在这个窗口上
    glfwMakeContextCurrent(window); 
    // 设置窗口的帧缓冲区大小变化时的回调函数。
    glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);

    /**
     * gladLoadGLLoader 是用来加载所有OpenGL函数指针的GLAD函数
     * glfwGetProcAddress 是GLFW的一个函数，用于根据函数名返回OpenGL函数的地址。
     */
    if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))
    {
        std::cout << "Failed to initialize GLAD" << std::endl;
        return -1;
    }

    // 创建了一个【顶点着色器】对象，并返回一个代表这个着色器的唯一标识符
    unsigned int vertexShader = glCreateShader(GL_VERTEX_SHADER);
    /**
     * glShaderSource参数：
     * 1. 着色器对象id
     * 2. 传递的源码数量
     * 3. 传递的源码字符串
     * 4. 传递的源码长度，如果是NULL，OpenGL会自动计算源码长度
     */
    glShaderSource(vertexShader, 1, &vertexShaderSource, NULL);
    // 编译着色器
    glCompileShader(vertexShader);
    int success;
    char infoLog[512];
    // 检查编译是否成功
    glGetShaderiv(vertexShader, GL_COMPILE_STATUS, &success);
    if (!success)
    {
        glGetShaderInfoLog(vertexShader, 512, NULL, infoLog);
        std::cout << "ERROR::SHADER::VERTEX::COMPILATION_FAILED\n" << infoLog << std::endl;
    }

    // 创建了一个【片段着色器】对象，并返回一个代表这个着色器的唯一标识符
    unsigned int fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
    glShaderSource(fragmentShader, 1, &fragmentShaderSource, NULL);
    // 编译着色器
    glCompileShader(fragmentShader);
    // 检查编译是否成功
    glGetShaderiv(fragmentShader, GL_COMPILE_STATUS, &success);
    if (!success)
    {
        glGetShaderInfoLog(fragmentShader, 512, NULL, infoLog);
        std::cout << "ERROR::SHADER::FRAGMENT::COMPILATION_FAILED\n" << infoLog << std::endl;
    }

    /**
     * 创建一个着色器程序对象
     * 着色器程序用于将一个或多个着色器（如顶点着色器、片段着色器）链接成一个可执行的单元，这个单元最终用于图形管线中渲染图形。
     */
    unsigned int shaderProgram = glCreateProgram();
    // 将之前编译的顶点着色器和片段着色器附加到着色器程序上
    glAttachShader(shaderProgram, vertexShader);
    glAttachShader(shaderProgram, fragmentShader);
    // 对附加的着色器进行链接操作。链接是一个将各个着色器组合成最终程序的过程，这个程序可以在GPU上执行
    glLinkProgram(shaderProgram);
    // 检查链接状态
    glGetProgramiv(shaderProgram, GL_LINK_STATUS, &success);
    if (!success) {
        glGetProgramInfoLog(shaderProgram, 512, NULL, infoLog);
        std::cout << "ERROR::SHADER::PROGRAM::LINKING_FAILED\n" << infoLog << std::endl;
    }
    // 链接完成后，可以删除着色器对象
    glDeleteShader(vertexShader);
    // 链接完成后，可以删除着色器对象
    glDeleteShader(fragmentShader);

    float vertices[] = {
        -0.5f, -0.5f, 0.0f, // left  
         0.5f, -0.5f, 0.0f, // right 
         0.0f,  0.5f, 0.0f  // top   
    }; 

    unsigned int VBO, VAO;
    // 生成一个顶点数组对象，将其ID存储在VAO
    glGenVertexArrays(1, &VAO);
    // 生成一个顶点缓冲对象，将其ID存储在VBO
    glGenBuffers(1, &VBO);
    // 绑定VAO，之后对顶点数组的配置将会应用于这个VAO。
    glBindVertexArray(VAO);

    // 绑定VBO，指定其类型为GL_ARRAY_BUFFER，用于顶点数据。
    glBindBuffer(GL_ARRAY_BUFFER, VBO);
    // 将顶点数据vertices复制到当前绑定的缓冲（VBO）
    glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);

    /**
     * 定义了如何解释VBO中的顶点数据。0是顶点属性的位置（layout location），3表示每个顶点属性的组件数量（这里是x, y, z三个浮点数），
     * GL_FLOAT指定数据类型，GL_FALSE表示不需要标准化，3 * sizeof(float)是步长（每个顶点的间隔），最后的(void*)0指定第一个组件在缓冲中的偏移量。
     */
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
    // 启用顶点属性0，之前通过glVertexAttribPointer配置的顶点属性现在将被使用。
    glEnableVertexAttribArray(0);

    // 解绑当前的VBO，避免之后不小心对其进行修改
    glBindBuffer(GL_ARRAY_BUFFER, 0); 
    // 解绑VAO，同样是为了防止不小心的修改
    glBindVertexArray(0); 

    // 检测是否有窗口关闭请求
    while (!glfwWindowShouldClose(window))
    {
        // 输入处理
        processInput(window);

        // 设置清除颜色，这是在清除颜色缓冲区时使用的颜色。这里的颜色是深青色。
        glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
        // 使用前面设置的清除颜色来清除颜色缓冲区，为新的绘制操作做准备。
        glClear(GL_COLOR_BUFFER_BIT);

        // 指定了之前链接好的着色器程序shaderProgram作为当前渲染状态的一部分
        glUseProgram(shaderProgram);
        // 绑定之前配置好的顶点数组对象（VAO）。VAO包含了顶点数据布局的信息，包括VBO的引用和顶点属性配置。
        glBindVertexArray(VAO);

        /**
         * GL_TRIANGLES：这是一个绘制模式，表示OpenGL应该将顶点数据解释为一系列的三角形。每三个顶点组成一个三角形。
         * 0：这是起始索引，表示从顶点数组的哪里开始绘制，这里是从第0个顶点开始。
         * 3：顶点数量，这里指示OpenGL绘制3个顶点，这正好是一个三角形。
         */
        glDrawArrays(GL_TRIANGLES, 0, 3);
 
        // 这个函数将前缓冲区和后缓冲区交换，如果你的窗口是双缓冲的话。这样可以显示最新渲染的图像，而用户看不到图像绘制的过程，从而避免画面撕裂现象。
        glfwSwapBuffers(window);
        // 处理如窗口移动、缩放、键盘按键或鼠标移动等事件。它会调用之前设置的回调函数（如键盘处理、窗口大小改变等）。
        glfwPollEvents();
    }

    // 删除顶点数组对象（VAO）。
    glDeleteVertexArrays(1, &VAO);
    // 删除顶点缓冲对象（VBO）。
    glDeleteBuffers(1, &VBO);
    // 删除着色器程序。
    glDeleteProgram(shaderProgram);

    // 清理所有由GLFW分配的资源
    glfwTerminate();
    return 0;
}